Title Banner

Previous Book Contents Book Index Next

Inside Macintosh: OpenDoc Cookbook / Part - Appendixes
Appendix A - OpenDoc Utilities


Exception Handling (Except)

This section describes the OpenDoc exception-handling utility, which resides in the files Except.h and Except.cpp. You can use the exception-handling utility to generate and handle your own exceptions as well as respond to any exceptions generated as a result of calls to OpenDoc. The exception-handling utility implements a simple throw-and-catch exception-handling scheme, similar to those found in some application frameworks and development environments.

Using the Exception-Handling Utility

The use of the exception-handling utility is optional. However, if you don't use it you must check the environment variable (ev) after every SOM call you make and handle it appropriately. The exception-handling utility facilitates this requirement, as described in "Handling SOM Exceptions".

To use the exception-handling utility, you add the file Except.cpp to your project (if you use a project-based system) or makefile (if you use MPW) and include Except.h in your source files.

IMPORTANT
You must include the header Except.h in your source files before including the headers of any SOM classes (.xh files in C++). If you precompile any SOM headers, you should also precompile Except.h and put it first among the headers that you precompile.
If you're building your project in debug mode (the symbol ODDebug is defined as 1), then Except.cpp will call functions from Crawl.cpp, and you'll need to add that source file to your project or makefile. Crawl.cpp in turn depends on ToolLibs.o (68K) or PPCToolsLib.o (PowerPC), which are part of your development system.

The Exception-Handling Scheme

In the kind of exception system implemented by the exception-handling utility, an error is signaled by being thrown, or made known to the system, by using the macro call THROW or one of its variants. The stack then unwinds back to the point where a calling function has set up an error handler for this exception. The handler then catches, or responds to, the exception; it performs whatever recovery or cleanup is necessary. The error handler can then allow execution to continue or--more commonly--it can reraise the exception, throwing it back to the next exception handler on the stack.

An exception handler in this scheme is defined as a block of code, delimited by the macro calls TRY, CATCH_ALL, and ENDTRY (or their variants). Only exceptions that are thrown within the scope of the TRY/ENDTRY pair of a handler can be handled by that handler.

The following is an example of the most basic use of this kind of exception system. It shows the exception handling involved with the fail-safe allocation of a pair of handles:

ODHandle MyNewHandle(ODSize size)
{
   OSErr err;
   ODHandle h = NewTempHandle(size,&err);
   THROW_IF_ERROR(err);
   return h;
}
This function (MyNewHandle) throws an exception if the NewTempHandle function returns a nonzero error. MyNewHandle does not itself catch any exceptions. The next function (TwoHandles) uses MyNewHandle to allocate the pair of handles:

void TwoHandles( )
{
   Handle h1, h2;
   h1 = MyNewHandle(10000);
   TRY{
      h2 = MyNewHandle(10000);
   }CATCH_ALL{
      ODDisposeHandle(h1);
      RERAISE;
   }ENDTRY
   ...
}
In TwoHandles, if the first call to MyNewHandle fails, it throws an error. Since TwoHandles has not set up an exception handler around that call, the exception is thrown out of TwoHandles and into its caller, and so on up the stack until an exception handler is found. None of the code shown here handles that exception.

If the first call succeeds, however, execution passes to the second call to MyNewHandle, which is inside an exception handler. If this call fails and throws an exception, the exception is caught by the exception handler, and the block after CATCH_ALL executes. This block cleans up by disposing of the first allocated handle (h1), preventing a memory leak. Thus, either both handles are allocated or neither is.

After h1 is disposed of, the exception handler calls RERAISE, which re-throws the same exception up the stack until the next enclosing exception handler is found. (If the handler hadn't called RERAISE, execution would have fallen out of the exception handler to the statement following ENDTRY.)

If the second call to MyNewHandle succeeds, execution falls out of the entire exception handler, skipping the CATCH_ALL block entirely (since there was no exception) and ending up at the statement immediately following ENDTRY.

Throwing Exceptions

In the OpenDoc exception-handling utility, you throw an exception by calling the THROW macro or one of its variants. This causes execution to jump immediately to the closest exception handler below it on the stack. These are the variants of THROW:

THROW

THROW throws an exception with the error number that is supplied to it. The error can be a standard OpenDoc error or a platform-specific error. The error code must be a nonzero value; it doesn't make sense to throw an exception whose value is kODNoError.

THROW_IF_NULL

THROW_IF_NULL throws the exception kODErrOutOfMemory if a null pointer is supplied to it. Call this macro after you call a memory-allocation function (such as SOMNew or MMNewPtr) that returns null when there is insufficient memory.

Do not use THROW_IF_NULL with functions that can return null for other reasons. For example, the Mac OS Resource Manager routine GetResource returns null if the resource cannot be found; in that case, you should first call ResError to find the actual error code, and then call THROW.

THROW_IF_ERROR

THROW_IF_ERROR throws an exception if the error supplied to it is nonzero. If the error value is kODNoError, nothing happens. This is a useful call to use following a function call whose return value is an error code.

For example, the Mac OS File Manager function FSpOpenDF returns zero if it succeeds, and otherwise a nonzero OSErr code. Passing the result to THROW_IF_ERROR ensures that the right exception is thrown if the call to FSpOpenDF fails.

Exception Handlers

An exception handler consists of a TRY block, zero or more CATCH_ALL blocks, and an ENDTRY:

TRY{
   // statements
}CATCH_ALL{
   // statements
}ENDTRY
It's perfectly legal lexically (and not uncommon) to nest exception handlers in a single function. Any error caught and reraised by the inner handler will be caught by the outer one.

The rest of this section describes what actions each of the macro statements and its associated code block perform.

TRY

Following a TRY macro, the immediately subsequent statements are executed. If one of the statements, or any function one of the statements calls, throws an exception that reaches this exception handler, then one of the following CATCH_ALL blocks may be executed. Otherwise, after the last statement in the TRY block finishes, control passes to the statement following the ENDTRY.

CATCH_ALL

If an exception is thrown to this handler, the statements following the CATCH_ALL macro are executed. To tell what error code was thrown, use the ErrorCode function.

The flow of control for the CATCH_ALL is the same as for CATCH. If no exception is raised or re-raised, control passes from the last statement in this CATCH_ALL block to the statement following the ENDTRY.

ENDTRY

The ENDTRY macro statement indicates the end of the exception handler. After a TRY or CATCH block finishes without throwing or re-raising an exception, the exception handler removes itself from the stack and control passes to the statement following ENDTRY.

RERAISE

The RERAISE macro statement is called within a CATCH_ALL block. It causes the exception to be thrown again, to the next active exception handler on the stack. This is the normal behavior for an exception handler--most of the time you don't want to hide the error; you want to propagate it so a higher level handler can deal with it.

The SOM Environment Parameter

OpenDoc objects are SOM objects, which means that they follow the CORBA rules for handling exceptions. Every method call made to an OpenDoc object (including your part, as a subclass of ODPart) must therefore include an environment parameter (ev), a pointer to a value that can describe an error. For example, the CreateLinkSource method of ODDraft has the following prototype (in IDL):

ODLinkSource CreateLinkSource(in ODPart part);
The method takes a single parameter, of type ODPart. To use this method, however, a caller in C++ must supply two parameters:

MyLinkSource = MyDraft->CreateLinkSource(ev, somSelf);
If execution of the method results in an error condition, the receiver of the call (the draft object in this case) must place an exception code in the value pointed to by ev and return. The caller must therefore examine the ev parameter after every call to a SOM object, to see if an exception has been raised.

All OpenDoc methods that you call, as well as all public methods of your part editor that you write, must return errors this way. What this means for your exception handling is that

The environment variable is passed along through a sequence of calls and can be used in calls to both SOM and C++ objects. For example, the environment variable is passed in these situations:

For more information on the environment parameter and exceptions, see SOMobjects Developer Toolkit Users Guide and SOMobjects Developer Toolkit Programmers Reference Manual from IBM.

Any exception-handling scheme that you use must support this method of passing exceptions. The OpenDoc utility described in this section helps you check the environment variable after each method call.

Handling SOM Exceptions

The exception-handling utility has some special features that simplify working with SOM. There are two reasons why these features are necessary:

This implies that the ev parameter must be checked for an error value after every call to a SOM method and that an exception raised in a SOM method or any function it calls must be caught and its error code stored in the ev parameter. The exception-handling utility includes functions to simplify these tasks, which are variants of the exception handling macros previously introduced:

SOM_TRY
SOM_CATCH_ALL
SOM_ENDTRY
These macros are identical to TRY, CATCH_ALL, and ENDTRY, except that when they catch an exception, they store the exception value in the method's ev parameter where the caller can see it.

Because you cannot throw an exception out of a SOM method, it is illegal to RERAISE in the SOM_CATCH_ALL block. You should exit the function normally by falling off the end or calling return (in C++ the former generates slightly better code).

IMPORTANT
SOM_ENDTRY works differently than ENDTRY in that its default behavior is to re-raise the exception by storing the error information in the Environment variable so it is propagated to the caller. (With ENDTRY you must explicitly reraise in your CATCH_ALL block or the exception will disappear.) If you don't want to return the exception to the caller, you must call SetErrorCode(kODNoError) in the SOM_CATCH block.

Automatic Environment Checking

If you include the header Except.h in your source files, it defines a special preprocessor symbol that modifies the way SOM messages are sent. Any SOM headers (.xh files for development in C++) included after Except.h has been included are modified so that, after the message is sent and control returns to the caller, the environment variable (ev) is checked and an exception raised if the variable contains an error.

For example, the following code fragment does not use automatic environment checking:

#include <ODWingDing.xh>...
long AFunction (Environment *ev, ODWingDing *wingDing)
{
   long result = wingDing->Spin(ev);
   if(ev->_major) {     // ODWingDing::Spin returns error
      result = 0;
      goto handle_error;
   }
   ...
handle_error:
   return result;
}
In this example, Except.h is not included, so environment checking is not automatic, and the caller (AFunction) can and must check the environment variable after every SOM method call.

Here is the same example with automatic environment checking:

#include <Except.h>     // Enables automatic ev checking
#include <ODWingDing.xh>...
long AFunction (Environment *ev, ODWingDing *wingDing)
{
   long result;
   SOM_TRY
      long result = wingDing->Spin(ev);
      ...
   SOM_CATCH_ALL
      result = kODNULL;
   SOM_ENDTRY
   return result;
}
Since Except.h is included before ODWingDing.h, environment-checking code is added to the call to the Spin method of ODWingDing. If Spin encounters an error and returns error status in ev, an exception with that same error code is thrown, which will be caught by the SOM_CATCH_ALL exception handler and in its turn returned in the ev parameter of AFunction.

There are two important precautions to keep in mind:

Coding Precautions

To achieve its results as a C++ library, the OpenDoc exception-handling utility relies on complex macros and sophisticated library functions. To some extent, it "fools" the compiler. Because of this, there are some precautions you have to take to avoid causing the compiler to generate incorrect code.

Very few C++ compilers have intrinsic support for exceptions, so the OpenDoc exception-handling utility is based on the ANSI setjmp and longjmp calls. Because of this basis, the compiler cannot always track the possible flow of control when exceptions are thrown and caught. The compiler can generate code that improperly fails to pop an exception handler off the stack or that makes incorrect assumptions about flow of execution.

This section discusses the precautions you must follow to make sure that the complier makes no mistakes, even when you have nested exception handlers.

Make Variables That You Modify Volatile

The compiler's register allocator and optimizer can make incorrect assumptions and generate bad code unless you take this precaution: always declare as volatile any variable or parameter that you modify in a TRY block and then use in a CATCH or CATCH_ALL block.

The reason for this is that the compiler doesn't understand that the TRY block can be executed on the way to a CATCH block, and that therefore the variable may be modified before the CATCH block is reached. It may therefore end up using an obsolete value for the variable while in the CATCH block. To work around this, you have to tell the compiler not to store the variable's value in a register (because it may be out of date) but always to look it up from the stack frame.

The C++ volatile keyword in the variable declaration does this (tells the compiler not to store the variable's value in a register). Unfortunately, some compilers don't implement it properly, and it can be confusing to use properly with pointer variables. For this reason the exception system defines a macro, ODVolatile, that declares a variable to be volatile. All you have to do is put this after the variable declaration. Here's an example:

void *p = kODNULL; ODVolatile(p);
TRY{
   Zog1();
   p = ODNewPtr(10000);
   Zog2();
}CATCH{
   ODDisposePtr(p);
   RERAISE;
}ENDTRY
The purpose of the exception handler is to make sure that p is disposed of on the way out in case it was allocated by ODNewPtr. Because p is modified inside the TRY block, it has to be marked as volatile. (Note that when the CATCH block is called, p might still be NULL--if Zog1 or ODNewPtr threw the exception--or it might be a valid pointer, if it was Zog2 that threw the exception. Fortunately, we pre-initialized p to kODNULL, and ODDisposePtr can safely be passed a null pointer. If we hadn't initialized p, this code might crash.)


Previous Book Contents Book Index Next

© Apple Computer, Inc.
16 JUL 1996




Navigation graphic, see text links

Main | Page One | What's New | Apple Computer, Inc. | Find It | Contact Us | Help